﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace StackExchangeApi
{
    /// <summary>
    /// Enumeration for the different classes of badges.
    /// </summary>
    public enum BadgeClass{
        /// <summary>
        /// Gold badge
        /// </summary>
        Gold,

        /// <summary>
        /// Silver badge
        /// </summary>
        Silver,

        /// <summary>
        /// Bronze badge
        /// </summary>
        Bronze
    }

    /// <summary>
    /// Enumeration for the types of badges
    /// </summary>
    public enum BadgeType
    {
        /// <summary>
        /// A badge that is universal to all  Trilogy Sites
        /// </summary>
        General,
        
        /// <summary>
        /// A badge that is dependent on the existance of a tag,
        /// and thus is (generally) found on only one Trilogy Site.
        /// </summary>
        Tag
    }

    /// <summary>
    /// Represents a badge on one of the Trilogy Sites.
    /// </summary>
    public sealed class Badge
    {
        private Object m_sync_obj = new Object();
        private String m_description;
        private ReadOnlyCollection<User> m_recipients;

        /// <summary>
        /// Id of the Badge
        /// </summary>
        public long Id
        {
            get;
            internal set;
        }

        /// <summary>
        /// Site the Badge is on
        /// </summary>
        public StackExchangeSite Site
        {
            get;
            internal set;
        }

        /// <summary>
        /// Name of the Badge
        /// </summary>
        public String Name
        {
            get;
            internal set;
        }

        /// <summary>
        /// Class (gold, silver, bronze) of the Badge
        /// </summary>
        public BadgeClass Class
        {
            get;
            internal set;
        }

        /// <summary>
        /// Type (general or tag) of the Badge
        /// </summary>
        public BadgeType BadgeType
        {
            get;
            internal set;
        }

        /// <summary>
        /// The Description of this Badge
        /// </summary>
        public String Description
        {
            get
            {
                if (m_description == null)
                {
                    lock (m_sync_obj)
                    {
                        if (m_description == null)
                            SetBadgeValues(this);
                    }
                }

                return m_description;
            }

            internal set
            {
                m_description = value;
            }
        }

        /// <summary>
        /// List of Users who have recently recieved this Badge.
        /// </summary>
        public ReadOnlyCollection<User> RecentRecipients
        {
            get
            {
                if (m_recipients == null)
                {
                    lock (m_sync_obj)
                    {
                        if (m_recipients == null)
                            SetBadgeValues(this);
                    }
                }

                return m_recipients;
            }

            set
            {
                m_recipients = value;
            }
        }

        /// <summary>
        /// Construct a new Badge
        /// </summary>
        /// <param name="id">Id of the Badge</param>
        /// <param name="name">Name of the Badge</param>
        /// <param name="description">Description of the Badge</param>
        /// <param name="site">Site the Badge is on</param>
        /// <param name="c">Class of the Badge</param>
        /// <param name="type">Type of the Badge</param>
        internal Badge(long id, String name, String description, StackExchangeSite site, BadgeClass c, BadgeType type)
        {
            Id = id;
            Name = name;
            Description = description;
            Site = site;
            Class = c;
            BadgeType = type;
        }

        /// <summary>
        /// Construct a new Badge.
        /// </summary>
        /// <param name="id">Id of the badge</param>
        /// <param name="name">Name of the badge</param>
        /// <param name="c">Class of the badge</param>
        /// <param name="site">Site the badge is on</param>
        /// <param name="type">Type of the badge</param>
        internal Badge(long id, String name, StackExchangeSite site, BadgeClass c, BadgeType type)
            : this(id, name, null, site, c, type) { }

        /// <summary>
        /// Get a Badge by name.
        /// 
        /// This Badge must be a "tag" based Badge, not a 
        /// general Badge.  General badges can be found in GeneralBadges.
        /// </summary>
        /// <param name="tag">Name of the tag</param>
        /// <param name="badgeClass">Class of the badge</param>
        /// <param name="site">Site the badge is on</param>
        /// <returns>The Badge if it exists, or null</returns>
        public static Badge GetTagBasedBadge(String tag, BadgeClass badgeClass, StackExchangeSite site)
        {
            if (!(tag.ToLowerInvariant().Equals(tag)))
                throw new ArgumentException("Tags are lower-case only");

            return TagBadgeCache.GetBadgeWith(tag, badgeClass, site);
        }

        /// <summary>
        /// Gets Badges by name.
        /// 
        /// These Badges must be "tag" based Badges, not
        /// general Badges.  General Badges can be found in GeneralBadges.
        /// </summary>
        /// <param name="tag">Name of the tag</param>
        /// <param name="site">Site the Badges are on</param>
        /// <returns>A List of Badges, which will be of Count 0 if the Badges cannot be found</returns>
        public static ICollection<Badge> GetTagBasedBadge(String tag, StackExchangeSite site)
        {
            List<Badge> badges = new List<Badge>();

            foreach (BadgeClass c in Enum.GetValues(typeof(BadgeClass)))
            {
                Badge badge = GetTagBasedBadge(tag, c, site);

                if (badge != null)
                    badges.Add(badge);
            }

            return badges;
        }

        /// <summary>
        /// Gets all the "tag based" Badges on the given "Trilogy Site".
        /// </summary>
        /// <param name="site">Site in question</param>
        /// <returns>A collection of all the Tag based badges.</returns>
        public static ICollection<Badge> GetTagBasedBadges(StackExchangeSite site)
        {
            return TagBadgeCache.GetAllTagBadges(site);
        }

        /// <summary>
        /// Retrieves a list of badges that the given User has.
        /// </summary>
        /// <param name="user">The user in question</param>
        /// <returns>A collection of badges that the User has received.</returns>
        internal static void SetBadgesForUser(User user)
        {
            List<Badge> badges = new List<Badge>();

            String profile = ConnectionMediator.MakeRequest(GetUserProfileUrl(user));
            
            //Make sure we start parsing at the Badge point
            String badgeStartStr = "<td class=\"summary-header\"><h1>Badges</h1></td>";
            int start = profile.IndexOf(badgeStartStr, StringComparison.Ordinal);
            start += badgeStartStr.Length;
            profile = profile.Substring(start);
            
            String splitStr = "/badges/";

            String[] pieces = profile.Split(new String[] { splitStr }, StringSplitOptions.None);
            for (int i = 1; i < pieces.Length; i++)
            {
                String piece = pieces[i];

                int p = piece.IndexOf("/", StringComparison.Ordinal);
                long id = Int64.Parse(piece.Substring(0, p), CultureInfo.InvariantCulture);

                Badge badge = GeneralBadges.GetBadgeBySiteAndId(user.Site, id);

                //If this is a tag based Badge (which means its not found on every site, for sure)
                if (badge == null)
                {
                    String titleStr = "title=\"";
                    int q = piece.IndexOf(titleStr, p, StringComparison.Ordinal);

                    int r = 0;
                    BadgeClass c = BadgeClass.Gold;
                    bool classSet = false;

                    if (q != -1)
                    {
                        q += titleStr.Length;
                        r = piece.IndexOf("\"", q, StringComparison.Ordinal);

                        String title = piece.Substring(q, r - q);

                        c = BadgeClassFromTitle(title);
                        classSet = true;
                    }

                    String badgeStr = "&nbsp;";
                    int s = piece.IndexOf(badgeStr, r, StringComparison.Ordinal);
                    s += badgeStr.Length;
                    int t = piece.IndexOf("<", s, StringComparison.Ordinal);

                    String name = piece.Substring(s, t - s);

                    if (classSet)
                        badge = TagBadgeCache.GetBadgeWith(id, name, user.Site, c);
                    else
                        badge = TagBadgeCache.GetBadgeWith(id, name, user.Site);
                    
                }

                int rep = 1;

                String repStr = "&times;&nbsp;";
                int u = piece.IndexOf(repStr, StringComparison.Ordinal);

                if (u != -1)
                {
                    u += repStr.Length;
                    int v = piece.IndexOf("<", u, StringComparison.Ordinal);
                    rep = Int32.Parse(piece.Substring(u, v - u), CultureInfo.InvariantCulture);
                }

                for(int j = 0; j < rep; j++)
                    badges.Add(badge);
            }

            user.Badges = badges.AsReadOnly();
        }

        /// <summary>
        /// Determine a Badge's class from its display title on a User's profile.
        /// 
        /// Throws an ArgumentException if no such inference is possible.
        /// </summary>
        /// <param name="title">Title to inspect</param>
        /// <returns>The corresponding BadgeClass</returns>
        internal static BadgeClass BadgeClassFromTitle(String title)
        {
            if (title.StartsWith("bronze badge: ", StringComparison.Ordinal)) return BadgeClass.Bronze;
            if (title.StartsWith("silver badge: ", StringComparison.Ordinal)) return BadgeClass.Silver;
            if (title.StartsWith("gold badge: ",   StringComparison.Ordinal)) return BadgeClass.Gold;

            throw new ArgumentException(
                String.Format(
                    CultureInfo.InvariantCulture,
                    "Expected a title that could be converted to a badge type, found [{0}]",
                    title));
        }

        /// <summary>
        /// Construct a Uri that when fetched will fetch the HTML version
        /// of a User's profile
        /// </summary>
        /// <param name="user">The User in question</param>
        /// <returns>As above</returns>
        private static Uri GetUserProfileUrl(User user)
        {
            String uri = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/users/{1}",
                Utils.GetAddressForSite(user.Site),
                user.Id);

            return new Uri(uri);
        }

        /// <summary>
        /// Determine a Badge's description by inspecting its page.
        /// 
        /// Sets the Description property on the given Badge
        /// </summary>
        /// <param name="badge">The Badge in question</param>
        private static void SetBadgeValues(Badge badge)
        {
            Uri uri = GetBadgePageUrl(badge.Id, badge.Site);
            String html = ConnectionMediator.MakeRequest(uri);

            SetBadgeRecentRecipients(html, badge);
            SetBadgeDescription(html, badge);
        }

        /// <summary>
        /// Sets the RecentRecipients of the given Badge given the HTML
        /// of the Badge's page.
        /// </summary>
        /// <param name="html">HTML of the Badge page</param>
        /// <param name="badge">Badge to populate</param>
        private static void SetBadgeRecentRecipients(String html, Badge badge)
        {
            List<User> recipients = new List<User>();

            String searchStr = "class='user-list'";
            int start = html.IndexOf(searchStr, StringComparison.Ordinal);
            start += searchStr.Length;

            html = html.Substring(start);

            String[] parts = html.Split(new String[] { "href=\"/users/" }, StringSplitOptions.None);

            for (int i = 1; i < parts.Length; i++)
            {
                String part = parts[i];

                int p = part.IndexOf("/", StringComparison.Ordinal);

                long id = Int64.Parse(part.Substring(0, p), CultureInfo.InvariantCulture);

                int q = part.IndexOf(">", p, StringComparison.Ordinal);
                q+=1;

                int r = part.IndexOf("<", q, StringComparison.Ordinal);

                String name = part.Substring(q, r - q);

                String repStr = "title=\"reputation score\">";
                int s = part.IndexOf(repStr, r, StringComparison.Ordinal);
                s += repStr.Length;

                int t = part.IndexOf("<", s, StringComparison.Ordinal);

                String rep = part.Substring(s, t - s);
                String repS = "";

                for (int j = 0; j < rep.Length; j++)
                    if (Char.IsDigit(rep[j])) repS += rep[j];

                long reputation = Int64.Parse(repS, CultureInfo.InvariantCulture);

                //TODO:  In the future, lets pull the badge counts out of this as well.

                User recipient = User.GetUserWithId(id, badge.Site);
                recipient.Name = name;
                recipient.Reputation = reputation;

                recipients.Add(recipient);
            }

            badge.RecentRecipients = recipients.AsReadOnly();
        }

        /// <summary>
        /// Sets the Description of the given Badge given the HTML
        /// of the Badge's page.
        /// </summary>
        /// <param name="html">HTML of the Badge page</param>
        /// <param name="badge">Badge to populate</param>
        private static void SetBadgeDescription(String html, Badge badge)
        {
            String searchStr = String.Format(
                CultureInfo.InvariantCulture,
                "</span>&nbsp;{0}",
                badge.Name.Replace("&", "&amp;"));

            int p = html.IndexOf(searchStr, StringComparison.Ordinal);

            if (p == -1)
                throw new ArgumentException(
                    String.Format(
                        CultureInfo.InvariantCulture,
                        "Badge.Name \"{0}\" does not match one found on Trilogy Site",
                        badge.Name));

            p += searchStr.Length;

            int q = html.IndexOf("<td>", p, StringComparison.Ordinal);
            q += 4;

            int r = html.IndexOf("</td>", q, StringComparison.Ordinal);

            String description = html.Substring(q, r - q).Replace("This badge can be awarded multiple times.", "");

            badge.Description = description.Trim();
        }

        /// <summary>
        /// Construct a Uri that when fetched will return the page
        /// that describes the provided Badge.
        /// </summary>
        /// <param name="id">Id of the Badge in question</param>
        /// <param name="site">Site the Badge is on</param>
        /// <returns>As above</returns>
        internal static Uri GetBadgePageUrl(long id, StackExchangeSite site)
        {
            String uri = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/badges/{1}",
                Utils.GetAddressForSite(site),
                id);

            return new Uri(uri);
        }

        /// <summary>
        /// Custom Equals implementation
        /// </summary>
        /// <param name="obj">Object to test for equality</param>
        /// <returns>true if equal, false if not</returns>
        public override bool Equals(object obj)
        {
            Badge other = obj as Badge;

            if (other == null) return false;

            return other.Site.Equals(Site)   &&
                   other.Name.Equals(Name)   &&
                   other.Class.Equals(Class) &&
                   other.BadgeType.Equals(BadgeType);
        }

        /// <summary>
        /// Custom GetHashCode implementation
        /// </summary>
        /// <returns>A hashcode for this Badge</returns>
        public override int GetHashCode()
        {
            return Name.GetHashCode() ^
                   Class.GetHashCode() ^
                   (BadgeType.GetHashCode() * -1);
        }

        #if DEBUG
        /// <summary>
        /// For debug purposes only.
        /// </summary>
        /// <returns>A string representation of this Badge</returns>
        public override string  ToString()
        {
            return Name + " @" + Id + " #" + Class + " %" + BadgeType;
        }
        #endif
    }

    /// <summary>
    /// Each "tag" Badge potentially takes
    /// a HTTP request to populate.  This
    /// class is used to cache these badges so
    /// we only ever pay for a Badge once.
    /// </summary>
    internal sealed class TagBadgeCache
    {
        private static volatile List<StackExchangeSite> TAG_BADGES_LOADED = new List<StackExchangeSite>();
        private static Dictionary<Pair<StackExchangeSite, long>, Badge> BADGE_CACHE = new Dictionary<Pair<StackExchangeSite, long>, Badge>();

        internal static Badge GetBadgeWith(long id, String name, StackExchangeSite site)
        {
            String html = ConnectionMediator.MakeRequest(Badge.GetBadgePageUrl(id, site));

            String searchStr = String.Format(
                CultureInfo.InvariantCulture,
                "/badges/{0}/",
                id);

            int p = html.IndexOf(searchStr, StringComparison.Ordinal);

            String titleStr = "title=\"";
            int q = html.IndexOf(titleStr, p, StringComparison.Ordinal);
            q += titleStr.Length;
            int r = html.IndexOf("\"", q, StringComparison.Ordinal);

            String title = html.Substring(q, r - q);

            return GetBadgeWith(id, name, site, Badge.BadgeClassFromTitle(title));
        }

        /// <summary>
        /// Create a "tag" Badge with the given parameters.
        /// 
        /// If one has been created in the past, instead return
        /// a reference to that Badge.
        /// </summary>
        /// <param name="id">Id of the Badge</param>
        /// <param name="name">Name of the Badge</param>
        /// <param name="site">Site the Badge is on</param>
        /// <param name="c">Class of the Badge</param>
        /// <returns>A "tag" Badge that mathces the given parameters.</returns>
        internal static Badge GetBadgeWith(long id, String name, StackExchangeSite site, BadgeClass c)
        {
            lock (BADGE_CACHE)
            {
                Pair<StackExchangeSite, long> key = new Pair<StackExchangeSite, long>(site, id);

                if (!BADGE_CACHE.ContainsKey(key))
                {
                    BADGE_CACHE[key] = new Badge(id, name, null, site, c, BadgeType.Tag);
                }

                return BADGE_CACHE[key];
            }
        }

        /// <summary>
        /// Create a "tag" Badge of the given name on the given site.
        /// 
        /// If one has been created in the past, return a reference to it.
        /// 
        /// The Badge returned may have properties that are lazily-loaded.
        /// </summary>
        /// <param name="name">Name of the badge</param>
        /// <param name="c">Class of the badge</param>
        /// <param name="site">Site the badge is on</param>
        /// <returns>The Badge, or null if it does not exist</returns>
        internal static Badge GetBadgeWith(String name, BadgeClass c, StackExchangeSite site)
        {
            lock (BADGE_CACHE)
            {
                foreach (Badge badge in BADGE_CACHE.Values)
                    if (badge.Name.Equals(name) && badge.Class.Equals(c) && badge.Site.Equals(site))
                        return badge;

                LoadTagBadges(site);

                foreach (Badge badge in BADGE_CACHE.Values)
                    if (badge.Name.Equals(name) && badge.Class.Equals(c) && badge.Site.Equals(site))
                        return badge;

                return null;
            }
        }

        /// <summary>
        /// Loads all the "tag" badges from the given site and places them into
        /// BADGE_CACHE.
        /// 
        /// If this has been called before with the same site, returns immediately.
        /// </summary>
        /// <param name="site">The "Trilogy Site" in question</param>
        internal static void LoadTagBadges(StackExchangeSite site)
        {
            lock (BADGE_CACHE)
            {
                //We've already loaded this site's tag badges
                if (TAG_BADGES_LOADED.Contains(site)) return;

                String html = ConnectionMediator.MakeRequest(GetTagBadgeUrl(site));

                String splitStr = "<a href=\"/badges/";
                String[] parts = html.Split(new String[] { splitStr }, StringSplitOptions.None);

                List<Badge> tagBadges = new List<Badge>();

                for (int i = 1; i < parts.Length; i++)
                {
                    String part = parts[i];

                    int a = part.IndexOf("/", StringComparison.Ordinal);
                    String idStr = part.Substring(0, a);

                    long id = Int64.Parse(idStr, CultureInfo.InvariantCulture);

                    String titleStr = "title=\"";
                    int p = part.IndexOf(titleStr, a, StringComparison.Ordinal);
                    p += titleStr.Length;
                    int q = part.IndexOf("\"", p, StringComparison.Ordinal);

                    String title = part.Substring(p, q - p);

                    BadgeClass c = Badge.BadgeClassFromTitle(title);

                    String nameStr = "&nbsp;";
                    int r = part.IndexOf(nameStr, q, StringComparison.Ordinal);
                    r += nameStr.Length;
                    int s = part.IndexOf("</a>", r, StringComparison.Ordinal);

                    String name = part.Substring(r, s - r);

                    tagBadges.Add(new Badge(id, name, title, site, c, BadgeType.Tag));
                }

                foreach (Badge badge in tagBadges)
                {
                    BADGE_CACHE[new Pair<StackExchangeSite, long>(badge.Site, badge.Id)] = badge;
                }

                TAG_BADGES_LOADED.Add(site);
            }
        }

        /// <summary>
        /// Construct a Uri that when fetched returns a page
        /// that lists all the "tag" based badges on the given site.
        /// </summary>
        /// <param name="site">The site in question</param>
        /// <returns>As above</returns>
        private static Uri GetTagBadgeUrl(StackExchangeSite site)
        {
            String uri = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/badges?tab=tags",
                Utils.GetAddressForSite(site));

            return new Uri(uri);
        }

        /// <summary>
        /// Returns all the "tag based" Badges on the given site.
        /// 
        /// Loads them if necessary.
        /// </summary>
        /// <param name="site">the site in question</param>
        /// <returns>As above</returns>
        internal static ICollection<Badge> GetAllTagBadges(StackExchangeSite site)
        {
            LoadTagBadges(site);

            List<Badge> badges = new List<Badge>();

            lock (BADGE_CACHE)
            {
                foreach (Badge badge in BADGE_CACHE.Values)
                {
                    if (badge.Site.Equals(site) && badge.BadgeType.Equals(BadgeType.Tag))
                        badges.Add(badge);
                }
            }

            return badges;
        }
    }
}